package atomutil

import "encoding/gob"
import "os"
import "path/filepath"
import "strings"
import "strconv"
import "crypto/rand"
import "math/big"

import "fmt"


const (
    DEBUG = true
)

//
// used to generate a UID for an operation
//
func nrand() int64 {
    max := big.NewInt(int64(1) << 62)
    bigx, _ := rand.Int(rand.Reader, max)
    x := bigx.Int64()
    return x
}

func IsDataExist(path string) (bool, error) {
    _, err := os.Stat(path)
    if err != nil {
        if os.IsNotExist(err) {
            return false, nil
        } else {
            if DEBUG { fmt.Printf("IsDataExists@atomutil: %v %v\n", path, err) }
            return false, err
        }
    } 
    return true, nil
}

func ReadData(path string, v interface{}) error {
    err2 := RecoveryAndClean(path)
    if err2 != nil { 
        if DEBUG { fmt.Printf("ReadData@atomutil: %v %v\n", path, err2) }
        return err2 
    }

    fin, err := os.Open(path)
    if err != nil { 
        if DEBUG { fmt.Printf("ReadData@atomutil: %v %v\n", path, err) }
        return err 
    }
    defer func() {
        if err := fin.Close(); err != nil {
            panic(err)
        }
    }()

    dec := gob.NewDecoder(fin)
    err = dec.Decode(v)
    if err != nil { 
        if DEBUG { fmt.Printf("ReadData@atomutil: %v %v\n", path, err) }
        return err 
    }

    return nil
}

func WriteData(path string, v interface{}) error {
    
    err := RecoveryAndClean(path)
    if err != nil { 
        if DEBUG { fmt.Printf("WriteData@atomutil: %v %v\n", path, err) }
        return err 
    }

    // Step W1: write data to a unique temporary file
    uid := nrand()
    tmp_path := path + ".tmp." + strconv.FormatInt(uid, 10)
    err = write_data_to_temp(tmp_path, v)
    if err != nil { 
        if DEBUG { fmt.Printf("WriteData@atomutil: %v %v\n", path, err) }
        return err 
    }

  
    // Step W2: rename the old file to a unique alternate form
    alt_path := path + ".alt." + strconv.FormatInt(uid, 10)
    _, err = os.Stat(path)      // check if the file exists
    if err != nil {
        if os.IsNotExist(err) {
            // file not exists
        } else {
            // other error
            if DEBUG { fmt.Printf("WriteData@atomutil: %v %v\n", path, err) }
            return err
        }
    } else {                    // the file exists. rename it!
        err = os.Rename(path, alt_path)
        if err != nil { 
            if DEBUG { fmt.Printf("WriteData@atomutil: %v %v\n", path, err) }
            return err 
        }
    }

    // Step W3: rename the new file into the old file
    err = os.Rename(tmp_path, path)
    if err != nil { 
        if DEBUG { fmt.Printf("WriteData@atomutil: %v %v\n", path, err) }
        return err 
    }

    // Step W4: (optinal) delete old alternate file
    os.Remove(alt_path)

    return nil
}

func RecoveryAndClean(path string) error {
    _, err := os.Stat(path)
    if err != nil {
        if os.IsNotExist(err) {

            lstTmpFiles, err2 := filepath.Glob(path + ".tmp.*")
            lstAltFiles, err3 := filepath.Glob(path + ".alt.*")
            if err2 != nil { return err2 }
            if err3 != nil { return err3 }

            // the file doesn't exist and we will write it the first time.  then
            // we do nothing in recovery phase
            if (lstTmpFiles == nil || len(lstTmpFiles) == 0) &&
                (lstAltFiles == nil || len(lstAltFiles) == 0) {
                return nil 
            }
            
            find := ""
            set := map[string]bool{}
            for _, file := range lstTmpFiles {
                uid := file[strings.LastIndex(file, ".") + 1:]
                set[uid] = true
            }
            for _, file := range lstAltFiles {
                uid := file[strings.LastIndex(file, ".") + 1:]
                _, exists := set[uid]
                if exists {
                    find = uid
                    break
                }
            }
            if find != "" {
                // Step R1: the file is missing but temporary file and alternate
                // file are present, the program crashed/failed between Step W2
                // W3. Put the alternate file back and retry from Step W1.
                err4 := os.Rename(path + ".alt." + find, path)
                if err4 != nil { return err4 }
                return nil
            } else {
                // Impossible. should exist both path.tmp.uid and path.alt.uid
                // for some uid
                panic("The original file (" + path + ") is missing!")
            }

        } else {
            return err
        }
    } else {  // the file exists

        lstTmpFiles, err2 := filepath.Glob(path + ".tmp.*")
        if err2 != nil { return err2 }
        if lstTmpFiles != nil && len(lstTmpFiles) > 0 {

            // Step R2: the file is present but temporary file is also present,
            // the program crashed/failed before Step W2. Delete all temp files
            // and ignore failures.
            for _, file := range lstTmpFiles {
                os.Remove(file)
            }

        }

        lstAltFiles, err3 := filepath.Glob(path + ".alt.*")
        if err3 != nil { return err3 }
        if lstAltFiles != nil && len(lstAltFiles) > 0 {

            // Step R3: the file is present but alternate file is also present,
            // the program crashed/failed between Step W3, W4. Delete all
            // alternate files and ignore failures.
            for _, file := range lstAltFiles {
                os.Remove(file)
            }

        }

    }

    return nil
}

func write_data_to_temp(tmp_path string, v interface{}) error {
    fout, err := os.Create(tmp_path)
    if err != nil { return err }
    defer func() {
        if err := fout.Close(); err != nil {
            panic(err)        
        }
    }()

    enc := gob.NewEncoder(fout)
    err = enc.Encode(v)
    if err != nil { return err }

    // force OS to commit data to disk
    //err = fout.Sync()
    //if err != nil { return err }  

    return nil
}
